#!/usr/bin/env bash
set -euo pipefail

# shellcheck source=lib/bats-core/formatter.bash
source "$BATS_ROOT/$BATS_LIBDIR/bats-core/formatter.bash"

init_suite() {
  suite_test_exec_time=0
  # since we have to print the suite header before its contents but we don't know the contents before the header,
  # we have to buffer the contents
  _suite_buffer=""
  test_result_state="" # declare for the first flush, when no test has been encountered
}

init_file() {
  file_count=0
  file_failures=0
  file_skipped=0
  file_exec_time=0
  test_exec_time=0
  name=""
  _buffer=""
  _buffer_log=""
  _system_out_log=""
  test_result_state="" # mark that no test has run in this file so far
}

host() {
  local hostname="${HOST:-}"
  [[ -z "$hostname" ]] && hostname="${HOSTNAME:-}"
  [[ -z "$hostname" ]] && hostname="$(uname -n)"
  [[ -z "$hostname" ]] && hostname="$(hostname -f)"

  echo "$hostname"
}

# convert $1 (time in milliseconds) to seconds
milliseconds_to_seconds() {
  # we cannot rely on having bc for this calculation
  full_seconds=$(($1 / 1000))
  remaining_milliseconds=$(($1 % 1000))
  if [[ $remaining_milliseconds -eq 0 ]]; then
    printf "%d" "$full_seconds"
  else
    printf "%d.%03d" "$full_seconds" "$remaining_milliseconds"
  fi
}

suite_header() {
  printf "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<testsuites time=\"%s\">\n" "$(milliseconds_to_seconds "${suite_test_exec_time}")"
}

file_header() {
  timestamp=$(date -u +"%Y-%m-%dT%H:%M:%S")
  printf "<testsuite name=\"%s\" tests=\"%s\" failures=\"%s\" errors=\"0\" skipped=\"%s\" time=\"%s\" timestamp=\"%s\" hostname=\"%s\">\n" \
    "$(xml_escape "${class}")" "${file_count}" "${file_failures}" "${file_skipped}" "$(milliseconds_to_seconds "${file_exec_time}")" "${timestamp}" "$(host)"
}

file_footer() {
  printf "</testsuite>\n"
}

suite_footer() {
  printf "</testsuites>\n"
}

print_test_case() {
  if [[ "$test_result_state" == ok && -z "$_system_out_log" && -z "$_buffer_log" ]]; then
    # pass and no output can be shortened
    printf "    <testcase classname=\"%s\" name=\"%s\" time=\"%s\" />\n" "$(xml_escape "${class}")" "$(xml_escape "${name}")" "$(milliseconds_to_seconds "${test_exec_time}")"
  else
    printf "    <testcase classname=\"%s\" name=\"%s\" time=\"%s\">\n" "$(xml_escape "${class}")" "$(xml_escape "${name}")" "$(milliseconds_to_seconds "${test_exec_time}")"
    if [[ -n "$_system_out_log" ]]; then
      printf "        <system-out>%s</system-out>\n" "$(xml_escape "${_system_out_log}")"
    fi
    if [[ -n "$_buffer_log" || "$test_result_state" == not_ok ]]; then
      printf "        <failure type=\"failure\">%s</failure>\n" "$(xml_escape "${_buffer_log}")"
    fi
    if [[ "$test_result_state" == skipped ]]; then
      printf "        <skipped>%s</skipped>\n" "$(xml_escape "$test_skip_message")"
    fi
    printf "    </testcase>\n"
  fi
}

xml_escape() { # <string to escape>
  local output=${1//&/\&amp;}
  output=${output//</\&lt;}
  output=${output//>/\&gt;}
  output=${output//'"'/\&quot;}
  output=${output//\'/\&#39;}
  # remove ANSI escape sequences (e.g. color codes, cursor movements)
  local CONTROL_CHAR=$'\033'
  local REGEX="$CONTROL_CHAR\[[0-9;]*[a-zA-Z]"
  while [[ "$output" =~ $REGEX ]]; do
      output=${output//${BASH_REMATCH[0]}/}
  done
  printf "%s" "$output"
}

suite_buffer() {
  local output
  output="$(
    "$@"
    printf "x"
  )" # use x marker to avoid losing trailing newlines
  _suite_buffer="${_suite_buffer}${output%x}"
}

suite_flush() {
  echo -n "${_suite_buffer}"
  _suite_buffer=""
}

buffer() {
  local output
  output="$(
    "$@"
    printf "x"
  )" # use x marker to avoid losing trailing newlines
  _buffer="${_buffer}${output%x}"
}

flush() {
  echo -n "${_buffer}"
  _buffer=""
}

log() {
  if [[ -n "$_buffer_log" ]]; then
    _buffer_log="${_buffer_log}
$1"
  else
    _buffer_log="$1"
  fi
}

flush_log() {
  if [[ -n "$test_result_state" ]]; then
    buffer print_test_case
  fi
  _buffer_log=""
  _system_out_log=""
  test_result_state="" # Clean out result from last test. Retried tests will have multiple begin calls.
}

log_system_out() {
  if [[ -n "$_system_out_log" ]]; then
    _system_out_log="${_system_out_log}
$1"
  else
    _system_out_log="$1"
  fi
}

finish_file() {
  if [[ "${class}" != JUNIT_FORMATTER_NO_FILE_ENCOUNTERED ]]; then
    file_header
    printf "%s\n" "${_buffer}"
    file_footer
    class=''
    name=''
  fi
}

finish_suite() {
  flush_log
  suite_header
  suite_flush
  finish_file # must come after suite flush to not print the last file before the others
  suite_footer
}

bats_tap_stream_plan() { #  <number of tests>
  :
}

bats_tap_stream_begin() { # <test index> <test name>
  flush_log
  # set after flushing to avoid overriding name of test
  name="$2"
}

bats_tap_stream_ok() { # <test index> <test name>
  test_exec_time=${BATS_FORMATTER_TEST_DURATION:-0}
  ((file_count += 1))
  test_result_state='ok'
  file_exec_time="$((file_exec_time + test_exec_time))"
  suite_test_exec_time=$((suite_test_exec_time + test_exec_time))
}

bats_tap_stream_skipped() { # <test index> <test name> <skip reason>
  test_exec_time=${BATS_FORMATTER_TEST_DURATION:-0}
  ((file_count += 1))
  ((file_skipped += 1))
  test_result_state='skipped'
  test_exec_time=0
  test_skip_message="$3"
}

bats_tap_stream_not_ok() { # <test index> <test name>
  if [[ -z "${name:-}" ]]; then
    # Can be called (after bats_tap_stream_plan) before anything else if the test fails in setup_suite
    bats_tap_stream_suite "setup_suite"
    bats_tap_stream_begin "$1" "$2"
  fi
  test_exec_time=${BATS_FORMATTER_TEST_DURATION:-0}
  ((file_count += 1))
  ((file_failures += 1))
  test_result_state=not_ok
  file_exec_time="$((file_exec_time + test_exec_time))"
  suite_test_exec_time=$((suite_test_exec_time + test_exec_time))
}

bats_tap_stream_comment() { # <comment text without leading '# '> <scope>
  local comment="$1" scope="$2"
  case "$scope" in
  begin)
    # everything that happens between begin and [not] ok is FD3 output from the test
    log_system_out "$comment"
    ;;
  ok)
    # non failed tests can produce FD3 output
    log_system_out "$comment"
    ;;
  *)
    # everything else is considered error output
    log "$1"
    ;;
  esac
}

bats_tap_stream_suite() { # <file name>
  flush_log
  suite_buffer finish_file
  init_file
  class="${1/$BASE_PATH/}"
}

bats_tap_stream_unknown() { # <full line>
  :
}

main() {
  local _buffer_log="" \
        name="" \
        class="JUNIT_FORMATTER_NO_FILE_ENCOUNTERED" \
        _buffer="" \
        _buffer_log="" \
        _suite_buffer="" \
        _system_out_log="" \
        test_result_state="" \
        test_skip_message=""

  local -i file_count=0 \
            file_failures=0 \
            file_skipped=0 \
            file_exec_time=0 \
            test_exec_time=0 \
            suite_test_exec_time=0 

  local BASE_PATH=.

  while [[ "$#" -ne 0 ]]; do
    case "$1" in
    --base-path)
      shift
      normalize_base_path BASE_PATH "$1"
      ;;
    esac
    shift
  done

  init_suite
  trap finish_suite EXIT
  trap '' INT

  bats_parse_internal_extended_tap

  trap - EXIT
  finish_suite # ensure we run this with the local variables still in scope
}

# [\x00-\x08\x0B\x0C\x0E-\x1F] can't use $'\x00' as this terminates the string early
BATS_JUNIT_FORMATTER_DELETE_CHARS_DEFAULT='\000'$'\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F'
main "$@" | tr -d "${BATS_JUNIT_FORMATTER_DELETE_CHARS-$BATS_JUNIT_FORMATTER_DELETE_CHARS_DEFAULT}"
